/*
 * Copyright (c) 2017, ARM Limited and Contributors. All rights reserved.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <arch_helpers.h>
#include <debug.h>
#include <io/io_block.h>
#include <mmio.h>
#include <platform_def.h>
#include <sys/types.h>
#include <utils_def.h>

#include "uniphier.h"

#define DIV_ROUND_UP(n, d)    (((n) + (d) - 1) / (d))

#define NAND_CMD_READ0        0
#define NAND_CMD_READSTART    0x30

#define DENALI_ECC_ENABLE            0x0e0
#define DENALI_PAGES_PER_BLOCK            0x150
#define DENALI_DEVICE_MAIN_AREA_SIZE        0x170
#define DENALI_DEVICE_SPARE_AREA_SIZE        0x180
#define DENALI_TWO_ROW_ADDR_CYCLES        0x190
#define DENALI_INTR_STATUS0            0x410
#define   DENALI_INTR_ECC_UNCOR_ERR            BIT(1)
#define   DENALI_INTR_DMA_CMD_COMP            BIT(2)
#define   DENALI_INTR_INT_ACT                BIT(12)

#define DENALI_DMA_ENABLE            0x700

#define DENALI_HOST_ADDR            0x00
#define DENALI_HOST_DATA            0x10

#define DENALI_MAP01                (1 << 26)
#define DENALI_MAP10                (2 << 26)
#define DENALI_MAP11                (3 << 26)

#define DENALI_MAP11_CMD            ((DENALI_MAP11) | 0)
#define DENALI_MAP11_ADDR            ((DENALI_MAP11) | 1)
#define DENALI_MAP11_DATA            ((DENALI_MAP11) | 2)

#define DENALI_ACCESS_DEFAULT_AREA        0x42

#define UNIPHIER_NAND_BBT_UNKNOWN        0xff

struct uniphier_nand {
    uintptr_t host_base;
    uintptr_t reg_base;
    int pages_per_block;
    int page_size;
    int two_row_addr_cycles;
    uint8_t bbt[16];
};

struct uniphier_nand uniphier_nand;

static void uniphier_nand_host_write(struct uniphier_nand *nand,
                     uint32_t addr, uint32_t data)
{
    mmio_write_32(nand->host_base + DENALI_HOST_ADDR, addr);
    mmio_write_32(nand->host_base + DENALI_HOST_DATA, data);
}

static uint32_t uniphier_nand_host_read(struct uniphier_nand *nand,
                    uint32_t addr)
{
    mmio_write_32(nand->host_base + DENALI_HOST_ADDR, addr);
    return mmio_read_32(nand->host_base + DENALI_HOST_DATA);
}

static int uniphier_nand_block_isbad(struct uniphier_nand *nand, int block)
{
    int page = nand->pages_per_block * block;
    int column = nand->page_size;
    uint8_t bbm;
    uint32_t status;
    int is_bad;

    /* use cache if available */
    if (block < ARRAY_SIZE(nand->bbt) &&
        nand->bbt[block] != UNIPHIER_NAND_BBT_UNKNOWN)
        return nand->bbt[block];

    mmio_write_32(nand->reg_base + DENALI_ECC_ENABLE, 0);

    mmio_write_32(nand->reg_base + DENALI_INTR_STATUS0, -1);

    uniphier_nand_host_write(nand, DENALI_MAP11_CMD, NAND_CMD_READ0);
    uniphier_nand_host_write(nand, DENALI_MAP11_ADDR, column & 0xff);
    uniphier_nand_host_write(nand, DENALI_MAP11_ADDR, (column >> 8) & 0xff);
    uniphier_nand_host_write(nand, DENALI_MAP11_ADDR, page & 0xff);
    uniphier_nand_host_write(nand, DENALI_MAP11_ADDR, (page >> 8) & 0xff);
    if (!nand->two_row_addr_cycles)
        uniphier_nand_host_write(nand, DENALI_MAP11_ADDR,
                     (page >> 16) & 0xff);
    uniphier_nand_host_write(nand, DENALI_MAP11_CMD, NAND_CMD_READSTART);

    do {
        status = mmio_read_32(nand->reg_base + DENALI_INTR_STATUS0);
    } while (!(status & DENALI_INTR_INT_ACT));

    bbm = uniphier_nand_host_read(nand, DENALI_MAP11_DATA);

    is_bad = bbm != 0xff;

    /* if possible, save the result for future re-use */
    if (block < ARRAY_SIZE(nand->bbt))
        nand->bbt[block] = is_bad;

    if (is_bad)
        WARN("found bad block at %d. skip.\n", block);

    return is_bad;
}

static int uniphier_nand_read_pages(struct uniphier_nand *nand, uintptr_t buf,
                    int page_start, int page_count)
{
    uint32_t status;

    mmio_write_32(nand->reg_base + DENALI_ECC_ENABLE, 1);
    mmio_write_32(nand->reg_base + DENALI_DMA_ENABLE, 1);

    mmio_write_32(nand->reg_base + DENALI_INTR_STATUS0, -1);

    /* use Data DMA (64bit) */
    mmio_write_32(nand->host_base + DENALI_HOST_ADDR,
              DENALI_MAP10 | page_start);

    /*
     * 1. setup transfer type, interrupt when complete,
     *    burst len = 64 bytes, the number of pages
     */
    mmio_write_32(nand->host_base + DENALI_HOST_DATA,
              0x01002000 | (64 << 16) | page_count);

    /* 2. set memory low address */
    mmio_write_32(nand->host_base + DENALI_HOST_DATA, buf);

    /* 3. set memory high address */
    mmio_write_32(nand->host_base + DENALI_HOST_DATA, buf >> 32);

    do {
        status = mmio_read_32(nand->reg_base + DENALI_INTR_STATUS0);
    } while (!(status & DENALI_INTR_DMA_CMD_COMP));

    mmio_write_32(nand->reg_base + DENALI_DMA_ENABLE, 0);

    if (status & DENALI_INTR_ECC_UNCOR_ERR) {
        ERROR("uncorrectable error in page range %d-%d",
              page_start, page_start + page_count - 1);
        return -EBADMSG;
    }

    return 0;
}

static size_t __uniphier_nand_read(struct uniphier_nand *nand, int lba,
                   uintptr_t buf, size_t size)
{
    int pages_per_block = nand->pages_per_block;
    int page_size = nand->page_size;
    int blocks_to_skip = lba / pages_per_block;
    int pages_to_read = DIV_ROUND_UP(size, page_size);
    int page = lba % pages_per_block;
    int block = 0;
    uintptr_t p = buf;
    int page_count, ret;

    while (blocks_to_skip) {
        ret = uniphier_nand_block_isbad(nand, block);
        if (ret < 0)
            goto out;

        if (!ret)
            blocks_to_skip--;

        block++;
    }

    while (pages_to_read) {
        ret = uniphier_nand_block_isbad(nand, block);
        if (ret < 0)
            goto out;

        if (ret) {
            block++;
            continue;
        }

        page_count = MIN(pages_per_block - page, pages_to_read);

        ret = uniphier_nand_read_pages(nand, p,
                           block * pages_per_block + page,
                           page_count);
        if (ret)
            goto out;

        block++;
        page = 0;
        p += page_size * page_count;
        pages_to_read -= page_count;
    }

out:
    /* number of read bytes */
    return MIN(size, p - buf);
}

static size_t uniphier_nand_read(int lba, uintptr_t buf, size_t size)
{
    size_t count;

    inv_dcache_range(buf, size);

    count = __uniphier_nand_read(&uniphier_nand, lba, buf, size);

    inv_dcache_range(buf, size);

    return count;
}

static struct io_block_dev_spec uniphier_nand_dev_spec = {
    .buffer = {
        .offset = UNIPHIER_BLOCK_BUF_BASE,
        .length = UNIPHIER_BLOCK_BUF_SIZE,
    },
    .ops = {
        .read = uniphier_nand_read,
    },
    /* fill .block_size at run-time */
};

static int uniphier_nand_hw_init(struct uniphier_nand *nand)
{
    int i;

    for (i = 0; i < ARRAY_SIZE(nand->bbt); i++)
        nand->bbt[i] = UNIPHIER_NAND_BBT_UNKNOWN;

    nand->host_base = 0x68000000;
    nand->reg_base = 0x68100000;

    nand->pages_per_block =
            mmio_read_32(nand->reg_base + DENALI_PAGES_PER_BLOCK);

    nand->page_size =
        mmio_read_32(nand->reg_base + DENALI_DEVICE_MAIN_AREA_SIZE);

    if (mmio_read_32(nand->reg_base + DENALI_TWO_ROW_ADDR_CYCLES) & BIT(0))
        nand->two_row_addr_cycles = 1;

    uniphier_nand_host_write(nand, DENALI_MAP10,
                 DENALI_ACCESS_DEFAULT_AREA);

    return 0;
}

int uniphier_nand_init(uintptr_t *block_dev_spec)
{
    int ret;

    ret = uniphier_nand_hw_init(&uniphier_nand);
    if (ret)
        return ret;

    uniphier_nand_dev_spec.block_size = uniphier_nand.page_size;

    *block_dev_spec = (uintptr_t)&uniphier_nand_dev_spec;

    return 0;
}
